/*
 * XDialog.java, created on Oct 20, 2005
 * 
 * Copyright (c) 2005 - 2007
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.xfc.dialog;

import java.awt.Container;
import java.awt.Cursor;
import java.awt.Dialog;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.util.ArrayList;
import java.util.List;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRootPane;
import javax.swing.JSeparator;
import javax.swing.KeyStroke;
import javax.swing.SwingConstants;

import org.xfc.XApp;
import org.xfc.XAppFrame;
import org.xfc.components.XFrame;
import org.xfc.util.XUtils;

import com.jgoodies.forms.layout.ColumnSpec;
import com.jgoodies.forms.layout.FormLayout;

// Layout should be:
//   [Help (1)         (2) OK Apply (3) Close Cancel (4)]

/**
 *
 *
 * @author Devon Carew
 */
public abstract class XDialog
	extends JDialog
{
	/** The normal position of the OK button - on the right side of the dialog, left of any Close or Cancel button. */
	public static final int OK_POSITION 	= 1;
	
	/** The normal position of the Close or Cancel button - on the right side of the dialog. */
	public static final int CLOSE_POSITION 	= 2;
	
	/** The normal position of the Help button - on the left side of the dialog. */
	public static final int HELP_POSITION 	= 3;
	
	protected int 		busyCount;
	
	private boolean 	constructed;
	
	private List 		helpActions = new ArrayList();
	private List 		okActions = new ArrayList();
	private List 		cancelActions = new ArrayList();
	
	private JLabel		errorLabel = new JLabel();
	private JButton		defaultButton;
	private Action		defaultAction;
	
	private Action  	closeWindowAction;
	
	private JPanel		contents;
	
	private Dimension	minimumSize;
	
	
	public XDialog()
	{
		this(XApp.getApp());
	}
	
	public XDialog(XApp app)
	{
		this(app.getActiveAppFrame());
	}
	
	public XDialog(XAppFrame appFrame)
	{
		this((Frame)appFrame);
	}
	
	/**
	 * @param parent
	 */
	public XDialog(Dialog parent)
	{
		super(parent);
		
		init();
	}

	/**
	 * @param parent
	 */
	public XDialog(Frame parent)
	{
		super(parent);
		
		init();
	}
	
	public XApp getApp()
	{
		if (getParent() instanceof XFrame)
			return ((XFrame)getParent()).getApp();
		
		return XApp.getApp();
	}
	
	/**
	 * @return Returns the contents.
	 */
	protected JPanel getContents()
	{
		return contents;
	}
	
	/**
	 * @param contents The contents to set.
	 */
	public void setContents(JPanel contents)
	{
		this.contents = contents;
	}
	
	/**
	 * Create the dialog, layout the components.
	 */
	protected final void construct()
	{
		construct(true);
	}
	
	/**
	 * Create the dialog, layout the components.
	 * 
	 * @param drawSeparator draw a horizontal separator between the content area and the buttons
	 */
	protected final void construct(boolean drawSeparator)
	{
		constructed = true;
		
		JPanel contents = getContents();
		
		if (contents == null)
			throw new IllegalStateException("XDialog contents must not be null");
		
		Container 	panel = getContentPane();
		JPanel 		buttonPanel = createButtonPanel();
		
		// If there are no buttons in the button panel, then have a smaller bottom border.
		int bottomBorder = buttonPanel.getPreferredSize().height < 5 ? 4 : 10;
		
		panel.setLayout(new FormLayout(
			"10dlu, pref:grow, 10dlu", 
			"10dlu, pref:grow, 6dlu, pref, pref, pref, pref, " + bottomBorder + "dlu"
		));
		
		panel.add(contents, "2, 2, f, f");
		
		errorLabel.setIcon(getApp().getResources().getIcon("/eclipse/org.eclipse.ui/icons/full/obj16/warn_tsk.gif"));
		errorLabel.setVisible(hasErrorMessage());
		errorLabel.setBorder(BorderFactory.createEmptyBorder(0, 0, 2, 0));
		
		panel.add(errorLabel, "2, 4, f, c");
		
		if (drawSeparator)
		{
			panel.add(new JSeparator(SwingConstants.HORIZONTAL), "2, 5, f, c");
			panel.add(Box.createVerticalStrut(6), "2, 6, f, c");
		}
		
		panel.add(buttonPanel, "2, 7, f, c");
	}
	
	/**
	 * An action that closes the dialog.
	 * 
	 * @return Returns the action that closes the dialog.
	 */
	protected Action createDefaultCloseAction()
	{
	    return new AbstractAction() {
	        public void actionPerformed(ActionEvent event) { setVisible(false); }
	    };
	}
	
	private JPanel createButtonPanel()
	{
		// -all buttons the same width
		// -buttons a minimum width
		// -special handing of the help section
		// -special handling of the ok / cancel sections
		
		FormLayout	layout = new FormLayout("", "pref");
		JPanel 		panel = new JPanel(layout);
		
		for (int i = 0; i < helpActions.size(); i++)
		{
			Action action = (Action)helpActions.get(i);
			
			if (i > 0)
				layout.appendColumn(new ColumnSpec("3dlu"));
			
			layout.appendColumn(new ColumnSpec("max(45dlu;pref)"));
			layout.addGroupedColumn(layout.getColumnCount());
			
			panel.add(createButton(action), layout.getColumnCount() + ", 1");
		}
		
		layout.appendColumn(new ColumnSpec("15dlu:grow"));
		
		for (int i = 0; i < okActions.size(); i++)
		{
			Action action = (Action)okActions.get(i);
			
			if (i > 0)
				layout.appendColumn(new ColumnSpec("3dlu"));
			
			layout.appendColumn(new ColumnSpec("max(45dlu;pref)"));
			layout.addGroupedColumn(layout.getColumnCount());
			
			panel.add(createButton(action), layout.getColumnCount() + ", 1");
		}
		
		if (cancelActions.size() > 0)
			layout.appendColumn(new ColumnSpec("5dlu"));
		
		for (int i = 0; i < cancelActions.size(); i++)
		{
			Action action = (Action)cancelActions.get(i);
			
			if (i > 0)
				layout.appendColumn(new ColumnSpec("3dlu"));
			
			layout.appendColumn(new ColumnSpec("max(45dlu;pref)"));
			layout.addGroupedColumn(layout.getColumnCount());
			
			panel.add(createButton(action), layout.getColumnCount() + ", 1");
		}
		
		return panel;
	}
	
	private JButton createButton(Action action)
	{
		JButton button = XApp.getComponentFactory().createButton(action);
		
		if (defaultAction == action)
			defaultButton = button;
		
		return button;
	}
	
	private void init()
	{
		setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
		
		addWindowListener(new WindowAdapter() {
			public void windowClosing(WindowEvent event) {
				closeAction();
			}
		});
		
		// We add a mouse listener to the glass pane so that when it is visible (when the frame is in
		// a busy state) it consumes mouse events instead of passing them through to other components.
		// TODO: how do users now cancel an operation in progress?
		getGlassPane().addMouseListener(new MouseAdapter() { });
	}
	
	protected JRootPane createRootPane()
	{
		JRootPane rootPane = super.createRootPane();
		KeyStroke stroke = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0);

		rootPane.registerKeyboardAction(new ActionListener() {
			public void actionPerformed(ActionEvent event) {
				closeAction();
			}
		}, stroke, JComponent.WHEN_IN_FOCUSED_WINDOW);

		return rootPane;
	}
	
	private void closeAction()
	{
		if (closeWindowAction != null)
			closeWindowAction.actionPerformed(new ActionEvent(this, 0, (String)closeWindowAction.getValue(Action.NAME)));
		else
		    createDefaultCloseAction().actionPerformed(null);
	}
	
	/**
	 * @param action
	 */
	public void setOKAction(Action action)
	{
		action.putValue(Action.NAME, XApp.getApp().getString("ok-action.text"));
		
		if (closeWindowAction == null)
			closeWindowAction = action;
		
		addAction(action, OK_POSITION, true);
	}
	
	/**
	 * @param action
	 */
	public void setHelpAction(Action action)
	{
		action.putValue(Action.NAME, XApp.getApp().getString("help-action.text"));
		
		addAction(action, HELP_POSITION, false);
	}
	
	// TODO: should these methods be protected?
	
	/**
	 * @param action
	 */
	public void setCancelAction(Action action)
	{
		action.putValue(Action.NAME, XApp.getApp().getString("cancel-action.text"));
		
		closeWindowAction = action;
		
		addAction(action, CLOSE_POSITION, true);
	}
	
	/**
	 * @param action
	 */
	public void setCloseAction(Action action)
	{
		action.putValue(Action.NAME, XApp.getApp().getString("close-action.text"));
		
		closeWindowAction = action;
		
		addAction(action, CLOSE_POSITION, true);
	}
	
	/**
	 * @param action
	 */
	public void setCloseWindowAction(Action action)
	{
		closeWindowAction = action;
	}
	
	/**
	 * @param action
	 * @param position
	 */
	public void addAction(Action action, int position)
	{
		addAction(action, position, false);
	}
	
	/**
	 * @param action
	 * @param position
	 * @param defaultButton
	 */
	public void addAction(Action action, int position, boolean defaultButton)
	{
		if (defaultButton && defaultAction == null)
			defaultAction = action;
		
		if (position == HELP_POSITION)
			helpActions.add(action);
		else if (position == OK_POSITION)
			okActions.add(action);
		else if (position == CLOSE_POSITION)
			cancelActions.add(action);
		else
			throw new IllegalArgumentException("Invalid value for position: " + position);
	}
	
	public void show()
	{
		if (!constructed)
			construct();
		
		if (defaultButton != null)
			getRootPane().setDefaultButton(defaultButton);
		
		pack();
		
		center();
		
		if (!isVisible() && getOwner() instanceof XFrame)
			((XFrame)getOwner()).setEnabled(false);
		
		super.show();
	}
	
	public void hide()
	{
		if (getOwner() instanceof XFrame)
			((XFrame)getOwner()).setEnabled(true);
		
		super.hide();
	}
	
	public void pack()
	{
		super.pack();
		
		if (getMinimumSize() != null)
		{
			Dimension size = getSize();
			Dimension minSize = getMinimumSize();
			
			if (size.width < minSize.width || size.height < minSize.height)
			{
				setSize(Math.max(size.width, minSize.width), Math.max(size.height, minSize.height));
			}
		}
	}

	protected void center()
	{
		if (getOwner() != null && getOwner().isVisible())
			XUtils.centerWindow(getOwner(), this);
	}
	
	public Dimension getMinimumSize()
	{
		return minimumSize;
	}
	
	public void setMinimumSize(Dimension minimumSize)
	{
		this.minimumSize = minimumSize;
	}

	/**
	 * @param busy
	 */
	public void setBusy(boolean busy)
	{
		boolean wasBusy = isBusy();
		
		busyCount += (busy ? 1 : -1);
		
		if (busyCount < 0)
			throw new IllegalStateException("Unmatched call to setBusy(false)");
		
		if (wasBusy && !isBusy())
		{
			// Turn off the busy indicators.
			getGlassPane().setCursor(null);
			getGlassPane().setVisible(false);
		}
		else if (!wasBusy && isBusy())
		{
			// Turn on the busy indicators.
			getGlassPane().setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
			getGlassPane().setVisible(true);
		}
	}
	
	/**
	 * @return whether this dialog is busy, i.e. showing a wait cursor
	 */
	public boolean isBusy()
	{
		return busyCount > 0;
	}
	
	public void setErrorMessage(String errorMessage)
	{
		errorLabel.setText(errorMessage);
		errorLabel.setVisible(hasErrorMessage());
	}
	
	public String getErrorMessage()
	{
		return errorLabel.getText();
	}
	
	public boolean hasErrorMessage()
	{
		String error = errorLabel.getText();
		
		return error != null && error.length() > 0;
	}
	
}
